Skip to main content

Custom Thumbnail Bar

You can use the <cylindo-viewer> with a custom thumbnail bar. Set the value of the slot attribute of the custom thumbnail bar to thumbnail-bar and use the methods and properties on the <cylindo-viewer> to control the thumbnail bar.

Simple custom thumbnail bar

Below, you will find a simple example to get you started.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const viewer = React.useRef(null);
  const [, forceUpdate] = useState({});

  // We wait for web-component to be registered
  useEffect(() => {
    customElements.whenDefined("cylindo-viewer").then(() => forceUpdate({}));
  }, []);

  // We force a state change to trigger a re-render, thus observing the properties of the viewer ref.
  const eventListener = () => forceUpdate({});

  // We add our event listeners to the viewer.
  useEffect(() => {
    if (viewer.current) {
      viewer.current.addEventListener("item-change", eventListener);
      viewer.current.addEventListener("loaded", eventListener);
      viewer.current.addEventListener("items-change", eventListener);
      viewer.current.addEventListener("frame-change", eventListener);
    }
  }, [viewer.current]);

  useEffect(() => {
    return () => {
      if (viewer.current) {
        viewer.current.removeEventListener("item-change", eventListener);
        viewer.current.removeEventListener("loaded", eventListener);
        viewer.current.removeEventListener("items-change", eventListener);
        viewer.current.removeEventListener("frame-change", eventListener);
      }
    };
  }, []);

  // Access the properties of the viewer needed for the thumbnail bar
  const { store, items, item } = viewer.current || {};
  const { viewerItemIndex } = item || {};

  return (
    <cylindo-viewer
      ref={viewer}
      customer-id="5098"
      code="ARMCHAIR-PDP"
      controls="ar qr fullscreen zoom"
    >
      <cylindo-studio code="RS-BARILA-A" customer-id="5098" />
      <cylindo-360-frame frame="10" />
      <cylindo-360-frame frame="16" />
      <cylindo-360 frame="3" />
      <cylindo-model />
      <div slot="thumbnail-bar" className="thumbnail-bar">
        {items &&
          items.map((item, index) => {
            const isSelected = viewerItemIndex === index;
            {
              /* Button where you can pass your own design,
                 or you can call the Content API (CAPI) to get content for each item. */
            }
            return (
              <button
                key={index}
                className={isSelected ? "selected" : ""}
                onClick={() =>
                  (viewer.current.item = {
                    viewerItem: item,
                    viewerItemIndex: index,
                  })
                }
              >
                {item.type}
              </button>
            );
          })}
      </div>
    </cylindo-viewer>
  );
}

Custom thumbnail bar with remote config

The following example demonstrates how to build a thumbnail bar for remote config.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const viewer = React.useRef(null);
  const [, forceUpdate] = useState({});

  // We wait for web-component to be registered
  useEffect(() => {
    customElements.whenDefined("cylindo-viewer").then(() => forceUpdate({}));
  }, []);

  // We force a state change to trigger a re-render, thus observing the properties of the viewer ref.
  const eventListener = () => forceUpdate({});

  // We add our event listeners to the viewer.
  useEffect(() => {
    if (viewer.current) {
      viewer.current.addEventListener("item-change", eventListener);
      viewer.current.addEventListener("loaded", eventListener);
      viewer.current.addEventListener("items-change", eventListener);
      viewer.current.addEventListener("frame-change", eventListener);
      viewer.current.addEventListener("config-change", eventListener);
    }
  }, [viewer.current]);

  useEffect(() => {
    return () => {
      if (viewer.current) {
        viewer.current.removeEventListener("item-change", eventListener);
        viewer.current.removeEventListener("loaded", eventListener);
        viewer.current.removeEventListener("items-change", eventListener);
        viewer.current.removeEventListener("frame-change", eventListener);
        viewer.current.removeEventListener("config-change", eventListener);
      }
    };
  }, []);

  // Access the properties of the viewer needed for the thumbnail bar
  const { store, items: localItems, item } = viewer.current || {};
  const { viewerItemIndex } = item || {};

  // The remote config items can be accessed through the store
  const { items: remoteConfigItems } = (store && store.get().config) || {};
  // Merge local items and remoteConfig items, if they exist.
  const items = [...(localItems || []), ...(remoteConfigItems || [])];

  return (
    <cylindo-viewer
      ref={viewer}
      customer-id="5098"
      code="SALSIE FF"
      remote-config="k2hctc08"
      controls="ar qr fullscreen zoom"
    >
      <div slot="thumbnail-bar" className="thumbnail-bar">
        {items &&
          items.map((item, index) => {
            const isSelected = viewerItemIndex === index;
            const features =
              item.features || (viewer.current ? viewer.current.features : {});
            {
              /* Button where you can pass your own design,
                 or you can call the Content API (CAPI) to get content for each item. */
            }
            return (
              <button
                key={index}
                className={isSelected ? "selected" : ""}
                onClick={() =>
                  (viewer.current.item = {
                    viewerItem: item,
                    viewerItemIndex: index,
                  })
                }
              >
                {item.type}
              </button>
            );
          })}
      </div>
    </cylindo-viewer>
  );
}

Custom thumbnail bar with different materials' variations

The example below shows a possible approach to creating a custom thumbnail bar with local items with different materials' variations. Clicking on these items will swap the materials' variations and change the current viewer item.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const viewer = React.useRef(null);
  const [, forceUpdate] = useState({});
  const customerId = 5098;
  const code = "WHISTLER SOFA BED";

  // We wait for web-component to be registered
  useEffect(() => {
    customElements.whenDefined("cylindo-viewer").then(() => forceUpdate({}));
  }, []);

  // We force a state change to trigger a re-render, thus observing the properties of the viewer ref.
  const eventListener = () => forceUpdate({});

  // We add our event listeners to the viewer.
  useEffect(() => {
    if (viewer.current) {
      viewer.current.addEventListener("item-change", eventListener);
      viewer.current.addEventListener("loaded", eventListener);
      viewer.current.addEventListener("features-change", eventListener);
      viewer.current.addEventListener("items-change", eventListener);
      viewer.current.addEventListener("frame-change", eventListener);
      viewer.current.addEventListener("config-change", eventListener);
    }
  }, [viewer.current]);

  useEffect(() => {
    return () => {
      if (viewer.current) {
        viewer.current.removeEventListener("item-change", eventListener);
        viewer.current.removeEventListener("features-change", eventListener);
        viewer.current.removeEventListener("loaded", eventListener);
        viewer.current.removeEventListener("items-change", eventListener);
        viewer.current.removeEventListener("frame-change", eventListener);
        viewer.current.removeEventListener("config-change", eventListener);
      }
    };
  }, []);

  // Access the properties of the viewer needed for the thumbnail bar
  const { store, items: localItems, item } = viewer.current || {};
  const { viewerItemIndex } = item || {};

  // The remote config items can be accessed through the store
  const { items: remoteConfigItems } = (store && store.get().config) || {};
  // Merge local items and remoteConfig items, if they exist.
  const items = [...(localItems || []), ...(remoteConfigItems || [])];

  return (
    <cylindo-viewer
      ref={viewer}
      customer-id={customerId}
      code={code}
      remote-config="k2hctc08"
      controls="ar qr fullscreen zoom"
    >
      <div slot="thumbnail-bar" className="thumbnail-bar">
        {items &&
          [
            ...items,
            // Append the custom items with different materials
            {
              type: "360",
              custom: true,
              features: { UPHOLSTERY: "MONTREAL SAND" },
              frame: 1,
            },
            {
              type: "360",
              custom: true,
              features: { UPHOLSTERY: "VICTORY TEAL" },
              frame: 16,
            },
            {
              type: "360StaticFrame",
              custom: true,
              features: { UPHOLSTERY: "ELEMENT EMERALD" },
              frame: 1,
            },
          ].map((item, index) => {
            const isSelected = viewerItemIndex === index;
            const features =
              item.features || (viewer.current ? viewer.current.features : {});

            return (
              <button
                key={index}
                className={`thumb ${isSelected ? "selected" : ""}`}
                onClick={() => {
                  // For our appended items
                  if (item.custom) {
                    // Find the corresponding item
                    const _item = items.find(i => i.type === item.type);
                    // Append the new feature (Material variation)
                    viewer.current.features = {
                      ...viewer.current.features,
                      ...item.features,
                    };
                    // Set the new item
                    viewer.current.item = {
                      viewerItem: _item,
                      viewerItemIndex: index,
                    };
                    if (item.frame) {
                      viewer.current.frame = item.frame;
                    }

                    return;
                  }

                  viewer.current.item = {
                    viewerItem: item,
                    viewerItemIndex: index,
                  };
                }}
              >
                <img
                  className="thumb-image"
                  src={getThumbnailImgSrc({
                    customerId,
                    code,
                    item,
                    features,
                  })}
                  alt={`Thumbnail bar item - ${item.type}`}
                  draggable="false"
                />
              </button>
            );
          })}
      </div>
    </cylindo-viewer>
  );

  // Get the thumbs images from the Content API
  function getThumbnailImgSrc({ item, customerId, code, features }) {
    const baseUrl = `https://content.cylindo.com/api/v2/${customerId}/products/`;
    // For the sake of this example, we take only the first feature available UPHOLSTERY.
    const featureOption = features["UPHOLSTERY"];
    const featuresParams = featureOption
      ? `&feature=UPHOLSTERY:${encodeURIComponent(featureOption)}`
      : "";
    switch (item.type) {
      case "studio":
        return `${baseUrl}${item.code}/frames/1/${item.code}.webp?size=105${featuresParams}`;
      case "360StaticFrame":
        return `${baseUrl}${code}/frames/${item.frame}/${code}.webp?size=105${featuresParams}`;
      case "360":
        return `${baseUrl}${code}/frames/${
          item.frame || 1
        }/${code}.webp?size=105${featuresParams}`;
      case "model":
        return `${baseUrl}${code}/frames/1/${code}.webp?size=105${featuresParams}`;
      case "dimensionShot":
        return `${baseUrl}${code}/dimensions/${code}.webp?dimensionCode=${item.dimensionCode}&dimensionLabelUnit=${item.dimensionLabelUnit}&size=105${featuresParams}`;
      case "swatch":
        return `${baseUrl}${code}/material/${code}.webp?crop=(32,32,64,64)&size=105&size=105&feature=UPHOLSTERY:${encodeURIComponent(
          featureOption || item.defaultOptionCode
        )}`;
      default:
        throw Error(`Unhandled item type: ${item.type}`);
    }
  }
}

The following code shows the styles used for all the examples above.

::part(thumbnail-bar-fullscreen-container) {
display: flex;
justify-content: center;
align-items: center;
padding: 4px 8px;
}
.thumbnail-bar {
display: flex;
gap: 4px;
display: flex;
column-gap: 10px;
margin: 10px 0;
align-items: center;
padding: 0 1em;
}

.thumb {
cursor: pointer;
padding: 0;
background-color: #abafad26;
border: 1px solid transparent;
border-radius: 8px;
padding: 4px;
box-sizing: content-box;
width: 64px;
height: 48px;
flex-shrink: 0;
transition: border 500ms;
}
.selected {
border: 1px solid #9bb0be;
}
.thumb-image {
display: flex;
justify-content: center;
align-items: center;
object-fit: cover;
width: 100%;
height: 100%;
border-radius: 4px;
}

Vertical Thumbnail Bar

This examples shows how to create a custom vertical thumbnail bar.

info

This interactive example demonstrates the concept using React. If you're using a different framework, adapt the core concepts to your preferred technology.

Result
Loading...
Live Editor
function Example() {
  const EVENTS_FORCE_UPDATE = [
    "item-change",
    "loaded",
    "items-change",
    "frame-change",
  ];

  const getThumbnailImgSrc = ({ item, customerId, code }) => {
    const baseUrl = `https://content.cylindo.com/api/v2/${customerId}/products/`;
    switch (item.type) {
      case "studio":
        return `${baseUrl}${item.code}/frames/1/${item.code}.webp?size=105`;
      case "360StaticFrame":
        return `${baseUrl}${code}/frames/${item.frame}/${code}.webp?size=105`;
      case "360":
        return `${baseUrl}${code}/frames/3/${code}.webp?size=105`;
      case "model":
        return `${baseUrl}${code}/frames/1/${code}.webp?size=105`;
      case "dimensionShot":
        return `${baseUrl}${code}/dimensions/${code}.webp?dimensionCode=${item.dimensionCode}&dimensionLabelUnit=${item.dimensionLabelUnit}&size=105`;
      default:
        throw Error(`Unhandled item type: ${item.type}`);
    }
  };

  const ChevronIcon = ({ direction }) => (
    <div
      style={{
        transform: `rotate(${direction === "up" ? "-" : ""}90deg)`,
      }}
    >

    </div>
  );
  const viewerRef = React.useRef(null);
  const [, setUpdateTrigger] = useState({});

  const forceUpdate = useCallback(() => setUpdateTrigger({}), []);

  // We wait for web-component to be registered
  useEffect(() => {
    customElements.whenDefined("cylindo-viewer").then(forceUpdate);
  }, [forceUpdate]);

  // We force a state change to trigger a re-render, thus observing the properties of the viewer ref.
  // We add our event listeners to the viewer.
  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;
    for (const eventType of EVENTS_FORCE_UPDATE) {
      viewer.addEventListener(eventType, forceUpdate);
    }
    return () => {
      for (const eventType of EVENTS_FORCE_UPDATE) {
        viewer.removeEventListener(eventType, forceUpdate);
      }
    };
  }, [forceUpdate]);

  const setItemIndex = itemIndex => {
    viewerRef.current.item = {
      viewerItem: items[itemIndex],
      viewerItemIndex: itemIndex,
    };
  };

  // Access the properties of the viewer needed for the thumbnail bar
  const { items, item } = viewerRef.current || {};
  const { viewerItemIndex } = item || {};

  const customerId = "5098";
  const code = "ARMCHAIR-PDP";

  return (
    <React.Fragment>
      <div className="wrapper">
        <cylindo-viewer ref={viewerRef} customer-id={customerId} code={code}>
          <cylindo-studio code="RS-BARILA-A" customer-id="5098" />
          <cylindo-360-frame frame="10" />
          <cylindo-360 frame="3" />
          <cylindo-model />
          <cylindo-dimension-shot dimension-code="UXPP" unit="Cm" />
        </cylindo-viewer>
        <div className="vertical-thumbnail-bar">
          <button
            className="nav"
            disabled={!items || viewerItemIndex === 0}
            onClick={() => setItemIndex(viewerItemIndex - 1)}
          >
            <ChevronIcon direction="up" />
          </button>

          {items &&
            items.map((item, index) => (
              <button
                key={index}
                className={viewerItemIndex === index ? "item selected" : "item"}
                onClick={() => setItemIndex(index)}
              >
                <img
                  src={getThumbnailImgSrc({ customerId, code, item })}
                  alt={`Thumbnail bar item - ${item.type}`}
                  draggable="false"
                />
              </button>
            ))}
          <button
            className="nav"
            disabled={!items || viewerItemIndex === items.length - 1}
            onClick={() => setItemIndex(viewerItemIndex + 1)}
          >
            <ChevronIcon direction="down" />
          </button>
        </div>
      </div>
    </React.Fragment>
  );
}

The following CSS code is applied to the previous example.

.wrapper {
display: flex;
height: 400px;
background-color: rgb(246, 247, 248);
}

.wrapper cylindo-viewer {
flex: 1;
}

.vertical-thumbnail-bar {
display: flex;
flex-direction: column;
width: 70px;
}

.vertical-thumbnail-bar button {
flex: 1;
color: #435d6d;
padding: 4px;

&:not(:disabled) {
cursor: pointer;

&:hover {
border-color: #ccd5da;
}
}
}

.item {
border: 1px solid transparent;
background: #abafad26;
border-radius: 8px;
transition: border 500ms;
height: 40px;
margin: 4px;

&.selected {
border-color: #9bb0be;
}
}

.item img {
height: auto;
}

.nav {
background: none;
border-radius: 50%;
border: none;
height: 30px;
transition: background 500ms;
margin: 0px 10px;

&:disabled {
opacity: 0.5;
}

&:not(:disabled)&:hover {
background: #e7edf1;
}
}